package eval.engine.FireFighting;


import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import com.google.inject.Inject;
import eval.deploy.DeploySet;
import eval.deploy.IDemandManager;
import eval.deploy.IDeployManager;
import eval.engine.IPlacement;
import eval.engine.IPlacementEngine;
import eval.engine.BasicEngineModel;
import eval.exceptions.CloseHostNotEmptytException;
import eval.host.Host;
import eval.host.IHostManager;
import eval.host.compare.ISortHostList;
import eval.host.compare.MaxLineHostSortList;
import eval.host.compare.MinLineHostSortList;
import eval.hostEvaluate.IHostEvaluate;
import eval.vm.IVMManager;
import eval.vm.MinCPURequiredComparator;
import eval.vm.VirtualMachine;

/**
 * This class is used to get the previous tick cloud state,
 * update it as needed (e.g. remove any VM which finish it life cycle),
 * than deploy the new set of VM's if needed or optimize the current VM's in the cloud (on optimization tick),
 * and transfer the new status to the FIRE_FIGHTING PLACEMENT.
 * 
 * @author Amit Peled   <amit.kapina@gmail.com>
 * @author Igal Kaufman <igal.kaufman@gmail.com> 
 */

public class FireFightingEngine extends BasicEngineModel implements IPlacementEngine {

	boolean isOngoingOptimizationTick;
	List<VirtualMachine> vmRelocate;
	Map<VirtualMachine, Integer> cpuDemandOfVMs;
	Map<VirtualMachine, Integer> memDemandOfVMs;

	/**
	 * @Constructor
	 * @param vmManager
	 * @param deployManager
	 * @param hostManager
	 * @param demandManager
	 * @param sortHostList
	 * @param hostEvaluator
	 */
	@Inject
	public FireFightingEngine(IVMManager vmManager, IDeployManager deployManager,
			IHostManager hostManager, IDemandManager demandManager,
			ISortHostList sortHostList,IHostEvaluate hostEvaluator) {
		super(vmManager,deployManager,hostManager,demandManager,sortHostList,hostEvaluator);
	}

	/* (non-Javadoc)
	 * @see eval.engine.IPlacementEngine#getEngineTimeInSec()
	 */
	@Override
	public double getEngineTimeInSec() {
		return engineStopTime - engineStartTime;
	}

	/* (non-Javadoc)
	 * @see eval.engine.IPlacementEngine#resetEngineTime()
	 */
	@Override
	public void resetEngineTime() {
		engineStartTime = engineStopTime = 0;

	}

	/* (non-Javadoc)
	 * @see eval.engine.IPlacementEngine#computePlacement(int, eval.engine.IPlacement)
	 */
	@Override
	public IPlacement computePlacement(int tick, IPlacement lastPlacement) {
		//update start time
		engineStartTime = System.currentTimeMillis();
		this.tick = tick;
		//cycle optimization
		isOngoingOptimizationTick = (tick % configuration.ongoingOptimizationPeriod) == 0;

		vmRelocate = new ArrayList<VirtualMachine>();
		//list of vm's that going to be deploy  
		List<VirtualMachine> placedNewVms =  new ArrayList<VirtualMachine>();
		//list of vm's that ask to deploy but rejected.
		List<VirtualMachine> rejectedVms =  new ArrayList<VirtualMachine>();
		//remove vm's list
		List<VirtualMachine> vmsToRemove = deployManager.getVMToBeRemovedAtTick(tick ,vmMapping);
		//deploy new VM list
		DeploySet deploySet = deployManager.getDeploySetAtTick(tick);
		//new deploy vm's
		List<VirtualMachine> vmsToDeploy = deploySet.getVMs();
		//CPU demand of vm's already on cloud
		cpuDemandOfVMs = demandManager.getCpuDemandOfAllVMsAtTick(tick);
		//MEM demand of vm's already on cloud
		memDemandOfVMs = demandManager.getMemDemandOfAllVMsAtTick(tick);
		//remove vm from host
		removeVirtualMachines(vmsToRemove);
		//Initial vmMaping and cloud
		iniCloudMapping(lastPlacement,vmsToRemove,cpuDemandOfVMs,memDemandOfVMs);
		if (!deploySet.isEmpty()) {  
			//we need to deploy new vm's
			// Order in which VMs are considered
			int vmCpuLoad,vmMemLOad;
			Collections.sort(vmsToDeploy,Collections.reverseOrder(new MinCPURequiredComparator()));
			for (VirtualMachine vm : vmsToDeploy) {
				Host host = getFirstFitHost(vm,cpuDemandOfVMs,memDemandOfVMs,tick);
				if(host != null){
					vmCpuLoad = Math.min(cpuDemandOfVMs.get(vm).intValue(),vm.getVMConfiguration().getMinCpu());
					vmMemLOad = memDemandOfVMs.get(vm).intValue();
					vmManager.setSatisfiedLoad(vm, vmCpuLoad, vmMemLOad);
					hostManager.addVmToHost(host,vm,vmCpuLoad, vmMemLOad);
					deployManager.setVmStartLife(vm, tick);
					placedNewVms.add(vm);
				}
				else{ //host not found
					rejectedVms.add(vm);
				}
			}

		}
		//update demand
		updateLoadOnCloud(cpuDemandOfVMs,memDemandOfVMs);
		//update map
		updateVmMap();
		HistoryDemandArray[tick] = getTotalCpuDemand(rejectedVms);
		//check if we need to open/close host
		if(rejectedVms.size() != 0){
			hostManager.openNewHost(tick);
		}
		openNewHost();
		//Optimization
		if(isOngoingOptimizationTick){
			optimizeCloud();
			updateVmMap();
		}
		else
			closeHost();
		//Validate placement
		validatePlacement();
		if(vmsToDeploy.size() != placedNewVms.size() + rejectedVms.size())
			System.out.println("Error at tick: "+tick);
		//Create placement
		FireFightingPlacement newPlacement = new FireFightingPlacement(
				vmMapping, hostManager, lastPlacement, demandManager ,vmsToRemove,vmsToDeploy,placedNewVms,rejectedVms ,tick ,vmRelocate,calcCloudGraphGradient());
		//Update end time
		engineStopTime = System.currentTimeMillis();
		return newPlacement;

	}	

	/* (non-Javadoc)
	 * @see eval.engine.BasicEngineModel#closeHost()
	 */
	@Override
	protected void closeHost () {
		int grad = calcCloudGraphGradient();
		//we estimate growing demand
		if(grad>=0)
			return;
		else if(super.getLastTurnOnHostTick() + configuration.numberOfTickClosingAfterOpening > tick){
			return;
		}
		for(Host host : hostManager.getOpenHosts()) {
			if(hostManager.getVmsAtHost(host).size()==0 && host.getTickRemainingForClosing()<=tick){
				hostManager.closeHost(host);
			}
		}
	}

	/*
	 * Optimization
	 */
	private void optimizeCloud() {
		//optimize hosts that above their MAX-LINE 
		optimizeMaxLineHost();
		//optimize candidate for closing
		List<Host> candidateForClosing = optimizeCandidateForCloseHost();
		//optimize host that under their MIN-LINE
		optimizeEvacuateHost(candidateForClosing);

	}

	/*
	 * optimize hosts that above their MAX-LINE 
	 * run on all hosts that load is above their MAX-LINE,
	 * and try to relocate VMs to another Host
	 * the relocation number limit by configuration file
	 */
	private void optimizeMaxLineHost() {
		int numberOfRelocation = configuration.numberOfRelocation;
		List<Host> openHosts = hostManager.getOpenHosts();
		boolean optimiztionFlag = true;
		boolean openHostFlag = false; 
		while(optimiztionFlag) {
			//sort host by max load
			Collections.sort(openHosts,new MaxLineHostSortList());
			Host hostToReduse = openHosts.get(0);
			double delta = getMaxLineDelta(hostToReduse);
			if(delta>0) {
				//if the host extends beyond is max line
				List<VirtualMachine> vmsAtHost = hostManager.getVmsAtHost(hostToReduse);
				Collections.sort(vmsAtHost,Collections.reverseOrder(new MinCPURequiredComparator()));
				List<VirtualMachine> vmsAtHostCopy = new CopyOnWriteArrayList<VirtualMachine>(vmsAtHost);
				for(VirtualMachine vmToRelocate : vmsAtHostCopy){ 
					//Iterate on vm's till you find the first fit VM
					if(!vmRelocate.contains(vmToRelocate) /*&& vmToRelocate.getVMConfiguration().getMinCpu()>= delta*/) {
						Host targetHost = findBestHostTargetForReduse(hostToReduse,vmToRelocate,delta, numberOfRelocation);
						if(targetHost!=null) {
							//We found the target host 
							int vmCpu = vmToRelocate.getVMConfiguration().getMinCpu();
							int vmMem = memDemandOfVMs.get(vmToRelocate);
							hostManager.relocateVm(vmToRelocate, hostToReduse, targetHost, vmCpu, vmMem);
							updateHostLoad(targetHost,hostManager.getVmsAtHost(targetHost),cpuDemandOfVMs,memDemandOfVMs);
							updateHostLoad(hostToReduse,hostManager.getVmsAtHost(hostToReduse),cpuDemandOfVMs,memDemandOfVMs);
							vmRelocate.add(vmToRelocate);
							numberOfRelocation--;
						} 
					}
					if(numberOfRelocation<=0)
						break;
				}
				delta = getMaxLineDelta(hostToReduse);
				if(delta>0) //if we have not succeed to balance the host
					openHostFlag = true;
			} 
			if(numberOfRelocation<=0) //We completed the relocation quota
				optimiztionFlag = false;
			else
				openHosts.remove(0);
			if(openHosts.size()==0)
				optimiztionFlag = false;
		}
		if(openHostFlag) {
			hostManager.openNewHost(tick);
		}
	}


	/*
	 * get the delta of MAX-LINE 
	 */
	private double getMaxLineDelta(Host host) {
		double hostLoad = host.getCurrentCpuLoad();
		double hostCapcity = host.getHostCapacity().getCpuCapacity();
		double maxLine = (configuration.MaxLine/100.0)*hostCapcity;
		double delta = hostLoad- maxLine;
		return delta;
	}

	/*
	 * find target host to relocate the VM 
	 */
	private Host findBestHostTargetForReduse(Host hostToOptimize,VirtualMachine vmToRelocate,double delta,int numberOfRelocation) {
		List<Host> openHosts = hostManager.getOpenHosts();
		openHosts.remove(hostToOptimize);
		if(openHosts.size() == 0) 
			return null;
		Collections.sort(openHosts,Collections.reverseOrder(new MaxLineHostSortList()));

		double targetMemCapcity = openHosts.get(0).getHostCapacity().getMemCapacity();
		double targetLoad = openHosts.get(0).getCurrentCpuLoad();
		double targetCpuCapcity = openHosts.get(0).getHostCapacity().getCpuCapacity();
		double targetMaxLine = (configuration.MaxLine/100.0)*targetCpuCapcity;
		double targetDelta = targetLoad - targetMaxLine;
		if(targetDelta>0)//all hosts loads above they red line - open new Host 
			return null;
		//check reservation and demand
		double minCpuAtTarget = vmToRelocate.getVMConfiguration().getMinCpu();
		double demandCpuAtTarget = cpuDemandOfVMs.get(vmToRelocate);
		double demandMemAtTarget = memDemandOfVMs.get(vmToRelocate);
		for(VirtualMachine vm : hostManager.getVmsAtHost(openHosts.get(0))) {
			minCpuAtTarget += vm.getVMConfiguration().getMinCpu();
			demandCpuAtTarget += cpuDemandOfVMs.get(vm);
			demandMemAtTarget += memDemandOfVMs.get(vm);
		}
		if(minCpuAtTarget>targetCpuCapcity || minCpuAtTarget>targetMaxLine || 
				demandCpuAtTarget>targetMaxLine ||
				demandMemAtTarget>targetMemCapcity) 
			//deploy at this host impossible - open new Host
			return null;
		//we found the  target host
		return openHosts.get(0);
	}


	/*
	 * optimize candidate for closing
	 */
	private List<Host> optimizeCandidateForCloseHost() {
		List<Host> openHosts = hostManager.getOpenHosts();
		List<Host> candidateForClosing = new ArrayList<Host>();
		for(Host host : openHosts) {
			if(hostNeedToEvacuation(host)) {
				candidateForClosing.add(host);
			}
		}
		return candidateForClosing;
	}

	/*
	 * return true if host is under is MIN-LINE
	 */
	private boolean hostNeedToEvacuation(Host host) {
		double hostLoad = host.getCurrentCpuLoad();
		double hostCapcity = host.getHostCapacity().getCpuCapacity();
		double hostMinline = (host.getHostCapacity().getMinLine() /100.0)*hostCapcity;
		double delta  = hostMinline - hostLoad;
		//Host load under minimum line
		if(delta>=0) {
			if(!host.isCandidateForClosing())//make host candidate
				hostManager.makeHostCandidateForClosing(host, tick+configuration.numberOfTickCandidatesForClosing);
			return true;
		}
		//Host load above minimum line
		if(host.isCandidateForClosing())//remove host for candidate category
			hostManager.removeHostFromCandidateForClosing(host);
		return false;
	}

	/*
	 * optimize candidate for closing
	 */
	private void optimizeEvacuateHost(List<Host> candidateForClosing) {
		List<Host> targetHostList = hostManager.getOpenHosts();
		targetHostList.removeAll(candidateForClosing);
		Collections.sort(candidateForClosing,Collections.reverseOrder(new MinLineHostSortList()));
		if(targetHostList.isEmpty()) //We have no target host to evacuate
			return;
		for(Host host : candidateForClosing) {
			if(host.getTickRemainingForClosing()<=tick)
				evaluatEvacuatHost(host,targetHostList,candidateForClosing);
		}
	}

	/*
	 *	check if we can evacuate host
	 */
	private void evaluatEvacuatHost(Host hostToClose, List<Host> targetHostList, 
			List<Host> candidateForClosing) {
		List<VirtualMachine> vmsToEvacuat = hostManager.getVmsAtHost(hostToClose);
		Map<Host,VirtualMachine> evacuatationHostTarget = new HashMap<Host, VirtualMachine>();
		for(VirtualMachine vm : vmsToEvacuat) {
			Host targetHost = findBestHostTargetForEvacuat(hostToClose,targetHostList,candidateForClosing,vm);
			if(targetHost != null) {
				evacuatationHostTarget.put(targetHost, vm);
			} else {
				//we not found target host for all the vm's
				return;
			}
		}
		if(evacuatationHostTarget.size()==hostManager.getVmsAtHost(hostToClose).size())
			evacuatHost(hostToClose,evacuatationHostTarget);
	}



	/*
	 * evacuate host
	 */
	private void evacuatHost(Host hostToClose,
			Map<Host, VirtualMachine> evacuatationHostTarget) {
		List<Host> tmp = new ArrayList<Host>(evacuatationHostTarget.keySet());
		for(Map.Entry<Host, VirtualMachine> it : evacuatationHostTarget.entrySet()) {
			VirtualMachine vm = it.getValue();
			Host target = it.getKey();
			hostManager.relocateVm(vm, hostToClose, target,vm.getVMConfiguration().getMinCpu() , vm.getVMConfiguration().getMinMem());
		}
		for(Host it : tmp) {
			Host target = it;
			updateHostLoad(target, hostManager.getVmsAtHost(target), cpuDemandOfVMs, memDemandOfVMs);
		}
		if(hostToClose.getCurrentCpuLoad()>0 || hostToClose.getCurrentMemLoad()>0 || !hostManager.getVmsAtHost(hostToClose).isEmpty())
			throw new CloseHostNotEmptytException(hostToClose, hostManager);
		hostManager.closeHost(hostToClose);		
	}



	/*
	 * find target host to relocate to him the VM
	 */
	private Host findBestHostTargetForEvacuat(Host hostToClose,
			List<Host> targetHostList, List<Host> candidateForClosing, VirtualMachine vmToRelocate) {
		for(Host host : targetHostList) { 
			double targetMemCapcity = host.getHostCapacity().getMemCapacity();
			double targetLoad = host.getCurrentCpuLoad();
			double targetCpuCapcity = host.getHostCapacity().getCpuCapacity();
			double targetMinLine = configuration.MaxLine / 100 *  targetCpuCapcity;

			double targetDelta = targetLoad - targetMinLine;
			if(targetDelta<=0) { 
				//check reservation and demand
				double minCpuAtTarget = vmToRelocate.getVMConfiguration().getMinCpu();
				double demandCpuAtTarget = cpuDemandOfVMs.get(vmToRelocate);
				double demandMemAtTarget = memDemandOfVMs.get(vmToRelocate);
				for(VirtualMachine vm : hostManager.getVmsAtHost(host )) {
					minCpuAtTarget += vm.getVMConfiguration().getMinCpu();
					demandCpuAtTarget += cpuDemandOfVMs.get(vm);
					demandMemAtTarget += memDemandOfVMs.get(vm);
				}
				if(minCpuAtTarget<=targetCpuCapcity && minCpuAtTarget<=targetMinLine && 
						demandCpuAtTarget<=targetMinLine &&
						demandMemAtTarget<=targetMemCapcity) 
					//we found the target host
					return host; 
			}
		} 
		//we don't find host at not candidate for closing search at rest hosts

		for(Host host : candidateForClosing) { 
			if(host.getHostId() == hostToClose.getHostId())
				continue;
			double targetMemCapcity = host.getHostCapacity().getMemCapacity();
			double targetLoad = host.getCurrentCpuLoad();
			double targetCpuCapcity = host.getHostCapacity().getCpuCapacity();
			double targetMinLine = (host.getHostCapacity().getMinLine()/100.0)*targetCpuCapcity;
			double targetDelta = targetLoad - targetMinLine;
			if(targetDelta<=0) { 
				//check reservation and demand
				double minCpuAtTarget = vmToRelocate.getVMConfiguration().getMinCpu();
				double demandCpuAtTarget = cpuDemandOfVMs.get(vmToRelocate);
				double demandMemAtTarget = memDemandOfVMs.get(vmToRelocate);
				for(VirtualMachine vm : hostManager.getVmsAtHost(host)) {
					minCpuAtTarget += vm.getVMConfiguration().getMinCpu();
					demandCpuAtTarget += cpuDemandOfVMs.get(vm);
					demandMemAtTarget += memDemandOfVMs.get(vm);
				}
				if(minCpuAtTarget<=targetCpuCapcity && minCpuAtTarget<=targetMinLine && 
						demandCpuAtTarget<=targetMinLine &&
						demandMemAtTarget<=targetMemCapcity) 
					//we found the best target host
					return host; 
			}
		}
		return null;
	}
}